package org.eclipse.assemblyformatter.ir;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.assemblyformatter.ir.lowlevel.CharacterLiteral;
import org.eclipse.assemblyformatter.ir.lowlevel.Comment;
import org.eclipse.assemblyformatter.ir.lowlevel.IntegerLiteral;
import org.eclipse.assemblyformatter.ir.lowlevel.LineSeparator;
import org.eclipse.assemblyformatter.ir.lowlevel.Symbol;
import org.eclipse.assemblyformatter.ir.lowlevel.WhiteSpace;
import org.eclipse.assemblyformatter.ir.lowlevel.Comment.Type;
import org.eclipse.assemblyformatter.parsers.IARParser;
import org.eclipse.assemblyformatter.parsers.Parser;
import org.w3c.dom.DOMException;

/**
 * General formatting procedure:
 * <ol>
 * <li>Identification of low-level elements (tokens, using automata).</li>
 * <li>Identification of high-level elements (using rules).</li>
 * <li>Resizing of white-space sections.</li>
 * </ol>
 * 
 */
public class Formatter {
	private IDocument document;
	private Section base;

	public Formatter(IDocument document) {
		this.document = document;
	}

	public void tokenize() {
		Tokenizer tokenizer = new Tokenizer();
		tokenizer.setContent(document.get());
		base = tokenizer.run();
	}

	/**
	 * 
	 * @throws BadLocationException
	 */
	public void parse() throws BadLocationException {
		Parser parser = new IARParser();
		parser.setBase(base);
		parser.setDocument(document);
		parser.run();
	}

	/**
	 * Align document content.
	 * 
	 * Call this after parsing.
	 * 
	 * <h3>Aligning procedure</h3>
	 * <ol>
	 * <li>Determine distances between vertical lines. (Pass through section
	 * list and see the longest label, instruction, etc.)</li>
	 * <li>Resize white-space sections.</li>
	 * </ol>
	 * 
	 * <table border="1" cellpadding="4">
	 * <tbody>
	 * <tr>
	 * <th>WHITE_SPACE between ...</th>
	 * <th>WHITE_SPACE length</th>
	 * </tr>
	 * <tr>
	 * <td>LINE_SEPARATOR [+ LABEL] and INSTRUCTION</td>
	 * <td>instructionDistance [- LABEL length]</td>
	 * </tr>
	 * <tr>
	 * <td>INSTRUCTION and PARAMETER</td>
	 * <td>parameterDistance - INSTRUCTION length</td>
	 * </tr>
	 * </tbody>
	 * </table>
	 * 
	 * TODO Label alignment to column 0
	 * 
	 * @throws BadLocationException
	 */
	public void rewrite() throws BadLocationException {
		Section section = null;

		// Determine distances between vertical lines.
		// TODO Case when no label is found
		int instructionDistance = 0;
		int parameterDistance = 0;
		int commentDistance = 0;

		section = base;
		while (section != null) {
			Section nextSection = section.getNextSection();

			final int length = section.getLength();

			if (length > 0) {
				// instruction distance
				if (section instanceof Label) {
					if (instructionDistance < length) {
						instructionDistance = length;
					}
				} else {
					// parameter distance
					if (section instanceof Instruction) {
						if (parameterDistance < length) {
							parameterDistance = length;
						}
					}
					// comment distance
					if (section instanceof Parameter) {
						if (commentDistance < length) {
							commentDistance = length;
						}
					}
				}
			}

			section = nextSection;
		}

		final int reserve = 4;

		// Resize white-space sections.
		section = base;

		while (section != null) {
			Section nextSection = section.getNextSection();

			if (nextSection == null) {
				break; // Exit loop.
			}

			if (section instanceof LineSeparator) {
				// Instruction aligning
				final int distance = instructionDistance + reserve;
				if (nextSection instanceof Label) {
					if (nextSection.nextIs(WhiteSpace.class, Instruction.class)) {
						resize(nextSection,
								(WhiteSpace) Section.getNextIs__staticData(0),
								distance);
					} else {
						if (nextSection.nextIs(Instruction.class)) {
							resize(nextSection, null, distance);
						}
					}
				} else {
					if (section.nextIs(WhiteSpace.class, Instruction.class)) {
						WhiteSpace whiteSpaceSection = (WhiteSpace) Section
								.getNextIs__staticData(0);
						whiteSpaceSection.setVirtualLength(distance);
					}
				}

				// Go forward.
				section = nextSection;
				continue;
			}

			if (section instanceof Instruction) {
				// Parameter aligning
				if (section.nextIs(WhiteSpace.class)) {
					if (nextSection.nextIs(Parameter.class)) {
						resize(section, (WhiteSpace) nextSection,
								parameterDistance + reserve);
					} else {
						// Comment aligning
						if (nextSection.nextIs(Comment.class)) {
							resize(section, (WhiteSpace) nextSection,
									parameterDistance + reserve
											+ commentDistance + reserve);
						}
					}
				}

				// Go forward.
				section = nextSection;
				continue;

			}

			if (section instanceof Parameter) {
				// Comment aligning
				final int distance = commentDistance + reserve;
				if (section.nextIs(WhiteSpace.class, Comment.class)) {
					resize(section, (WhiteSpace) nextSection, distance);
				}
			}

			section = nextSection;
		}

		// Connect position objects to document.
		section = base;
		while (section != null) {
			Section nextSection = section.getNextSection();
			try {
				document.addPosition(section.getPosition());
			} catch (BadLocationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			section = nextSection;
		}

		// Rewrite white-space sections.
		section = base;
		while (section != null) {
			Section nextSection = section.getNextSection();
			if (section instanceof WhiteSpace) {
				WhiteSpace whiteSpace = (WhiteSpace) section;
				if (whiteSpace.getVirtualLength() >= 0) {
					int offset = whiteSpace.getOffset();
					int length = whiteSpace.getLength();
					String text = whiteSpace.getVirtualString();
					try {
						document.replace(offset, length, text);
					} catch (BadLocationException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
			section = nextSection;
		}

		// Normalize comments.
		// In case ;... make ; ...
		int offset;
		section = base;
		while (section != null) {
			Section nextSection = section.getNextSection();
			if (section instanceof Comment) {
				Comment comment = (Comment) section;
				switch (comment.getType()) {
				case A:
					offset = comment.getOffset() + 1;
					if (document.getChar(offset) != ' ') {
						document.replace(offset, 0, " ");
					}
					break;
				case CPP:
					offset = comment.getOffset() + 2;
					if (document.getChar(offset) != ' ') {
						document.replace(offset, 0, " ");
					}
					break;
				}
			}
			section = nextSection;
		}

		// Disconnect position objects from document.
		section = base;
		while (section != null) {
			Section nextSection = section.getNextSection();
			document.removePosition(section.getPosition());
			section = nextSection;
		}
	}

	/**
	 * 
	 * @param section
	 * @param whiteSpace
	 *            can be null
	 * @param whiteSpaceLength
	 */
	private void resize(Section section, WhiteSpace whiteSpace,
			int whiteSpaceLength) {
		final int sectionLength = section.getLength();

		// Null WHITE_SPACE section case.
		if (whiteSpace == null) {
			whiteSpace = new WhiteSpace();
			whiteSpace.setOffset(section.getOffset() + sectionLength);
			whiteSpace.setLength(0);
			whiteSpace.setNextSection(section.getNextSection());
			section.setNextSection(whiteSpace);
		}

		whiteSpace.setVirtualLength(whiteSpaceLength - sectionLength);
	}

	/**
	 * Writes the content of the linked list of document sections obtained after
	 * run().
	 * 
	 * This is for verification/debugging purposes.
	 * 
	 * @throws ParserConfigurationException
	 * @throws IOException
	 * @throws TransformerException
	 */
	public void writeSectionList(String filename)
			throws ParserConfigurationException, IOException,
			TransformerException {
		PrintWriter outputStream = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory
					.newInstance();

			DocumentBuilder builder = factory.newDocumentBuilder();
			org.w3c.dom.Document domDocument;
			domDocument = builder.newDocument();

			// Set document content
			org.w3c.dom.Element elementDocument = domDocument
					.createElement("document");
			domDocument.appendChild(elementDocument);

			Section section = base;
			while (section != null) {
				Section nextSection = section.getNextSection();
				org.w3c.dom.Element elementSection = domDocument
						.createElement(section.getClass().getSimpleName());

				org.w3c.dom.CDATASection cdata;
				String sectionContent;
				try {
					sectionContent = section.getContent(document);
				} catch (BadLocationException e) {
					sectionContent = e.getClass().getSimpleName();
				}
				cdata = domDocument.createCDATASection(sectionContent);

				elementSection.setAttribute("offset",
						Integer.toString(section.getOffset()));
				elementSection.setAttribute("length",
						Integer.toString(section.getLength()));

				if (section instanceof WhiteSpace) {
					WhiteSpace whiteSpace = (WhiteSpace) section;
					if (whiteSpace.getVirtualLength() >= 0) {
						elementSection
								.setAttribute("virtualLength",
										Integer.toString(whiteSpace
												.getVirtualLength()));
					}
				}

				elementSection.appendChild(cdata);
				elementDocument.appendChild(elementSection);
				section = nextSection;
			}

			// Use a Transformer for output
			TransformerFactory tFactory = TransformerFactory.newInstance();
			Transformer transformer = tFactory.newTransformer();

			DOMSource source = new DOMSource(domDocument);

			outputStream = new PrintWriter(new FileWriter(filename));

			StreamResult result = new StreamResult(outputStream);
			transformer.transform(source, result);
		} catch (ParserConfigurationException e) {
			throw e;
		} catch (IOException e) {
			throw e;
		} catch (TransformerConfigurationException e) {
			throw e;
		} catch (TransformerException e) {
			throw e;
		} finally {
			if (outputStream != null) {
				outputStream.close();
			}
		}
	}

	/**
	 * This procedure replaces any TAB character in the document with a fixed
	 * number of space characters. Call this procedure before tokenization.
	 * 
	 * @throws BadLocationException
	 */
	public void replaceAnyTab(final int spaceCharCount)
			throws BadLocationException {
		if (spaceCharCount > (1 << 5)/* Maximum allowed space characters */) {
			// Error
			return;
		}

		StringBuilder strBuilder = new StringBuilder();
		for (int i = 0; i < spaceCharCount; i++) {
			strBuilder.append(' ');
		}
		final String space = strBuilder.toString();

		for (int offset = 0; offset < document.getLength(); offset++) {
			if (document.getChar(offset) == '\t') {
				document.replace(offset, 1, space);
				offset += spaceCharCount - 1; // Jump over the space inserted
			}
		}
	}
}
